/* * Copyright 2012, Oracle and/or its affiliates. All rights reserved. * * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.oracle.appbundler; import java.io.BufferedOutputStream; import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Writer; import java.net.URL; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Iterator; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.Task; import org.apache.tools.ant.types.FileSet; import org.apache.tools.ant.types.Reference; import org.apache.tools.ant.types.resources.FileResource; /** * App bundler Ant task. */ public class AppBundlerTask extends Task { // Output folder for generated bundle private File outputDirectory = null; // General bundle properties private String name = null; private String displayName = null; private String identifier = null; private File iconFile = null; private String executableName = EXECUTABLE_NAME; private String shortVersion = null; //"1.0"; private String version = null; //"1.0"; private String signature = "????"; private String copyright = null; //""; private String getInfo = null; private String privileged = null; private String workingDirectory = null; private String applicationCategory = null; private boolean highResolutionCapable = true; // Oracle Java 7 requires 10.7.3 or later, so require it here. private String minimumSystem = "10.7.3"; // By default, don't embed Java FX. private boolean javafx = false; // JVM info properties private String mainClassName = null; private FileSet runtime = null; private ArrayList<FileSet> classPath = new ArrayList<>(); private ArrayList<FileSet> libraryPath = new ArrayList<>(); private ArrayList<String> options = new ArrayList<>(); private ArrayList<String> arguments = new ArrayList<>(); private ArrayList<String> architectures = new ArrayList<>(); private ArrayList<BundleDocument> bundleDocuments = new ArrayList<>(); private Reference classPathRef; private static final String EXECUTABLE_NAME = "JavaAppLauncher"; private static final String DEFAULT_ICON_NAME = "GenericApp.icns"; private static final String OS_TYPE_CODE = "APPL"; private static final int BUFFER_SIZE = 2048; public void setOutputDirectory(File outputDirectory) { this.outputDirectory = outputDirectory; } public void setName(String name) { this.name = name; } public void setDisplayName(String displayName) { this.displayName = displayName; } public void setIdentifier(String identifier) { this.identifier = identifier; } public void setIcon(File icon) { this.iconFile = icon; } public void setExecutableName(String executable) { this.executableName = executable; } public void setShortVersion(String shortVersion) { this.shortVersion = shortVersion; } public void setVersion(String version) { this.version = version; } public void setSignature(String signature) { this.signature = signature; } public void setCopyright(String copyright) { this.copyright = copyright; } public void setGetInfo(String getInfo) { this.getInfo = getInfo; } public void setPrivileged(String privileged) { this.privileged = privileged; } public void setWorkingDirectory(String workingDirectory) { this.workingDirectory = workingDirectory; } public void setApplicationCategory(String applicationCategory) { this.applicationCategory = applicationCategory; } public void setMinimumSystem(String minimumSystem) { this.minimumSystem = minimumSystem; } public void setHighResolutionCapable(boolean highResolutionCapable) { this.highResolutionCapable = highResolutionCapable; } public void setJavaFX(boolean javafx) { this.javafx = javafx; } public void setMainClassName(String mainClassName) { this.mainClassName = mainClassName; } public void addConfiguredRuntime(FileSet runtime) throws BuildException { if (this.runtime != null) { throw new BuildException("Runtime already specified."); } this.runtime = runtime; runtime.appendIncludes(new String[] { "jre/", }); runtime.appendExcludes(new String[] { "bin/", // original version, removed entire bin folder // "jre/bin/", // remove everything except 'java' "jre/bin/keytool", "jre/bin/orbd", "jre/bin/pack200", "jre/bin/policytool", "jre/bin/rmid", "jre/bin/rmiregistry", "jre/bin/servertool", "jre/bin/tnameserv", "jre/bin/unpack200", "jre/lib/deploy/", "jre/lib/deploy.jar", "jre/lib/javaws.jar", "jre/lib/libdeploy.dylib", "jre/lib/libnpjp2.dylib", "jre/lib/plugin.jar", "jre/lib/security/javaws.policy" }); if (!javafx) { // http://www.oracle.com/technetwork/java/javase/jdk-7-readme-429198.html runtime.appendExcludes(new String[] { "jre/THIRDPARTYLICENSEREADME-JAVAFX.txt", "jre/lib/javafx.properties", "jre/lib/jfxrt.jar", "jre/lib/security/javafx.policy", "jre/lib/fxplugins.dylib", "jre/lib/libdecora-sse.dylib", "jre/lib/libglass.dylib", "jre/lib/libglib-2.0.0.dylib", "jre/lib/libgstplugins-lite.dylib", "jre/lib/libgstreamer-lite.dylib", "jre/lib/libjavafx-font.dylib", "jre/lib/libjavafx-iio.dylib", "jre/lib/libjfxmedia.dylib", "jre/lib/libjfxwebkit.dylib", "jre/lib/libprism-es2.dylib" }); } } public void setClasspathRef(Reference ref) { this.classPathRef = ref; } public void addConfiguredClassPath(FileSet classPath) { this.classPath.add(classPath); } public void addConfiguredLibraryPath(FileSet libraryPath) { this.libraryPath.add(libraryPath); } public void addConfiguredBundleDocument(BundleDocument document) { this.bundleDocuments.add(document); } public void addConfiguredOption(Option option) throws BuildException { String value = option.getValue(); if (value == null) { throw new BuildException("Value is required."); } options.add(value); } public void addConfiguredArgument(Argument argument) throws BuildException { String value = argument.getValue(); if (value == null) { throw new BuildException("Value is required."); } arguments.add(value); } public void addConfiguredArch(Architecture architecture) throws BuildException { String name = architecture.getName(); if (name == null) { throw new BuildException("Name is required."); } architectures.add(name); } @Override public void execute() throws BuildException { // Validate required properties if (outputDirectory == null) { throw new IllegalStateException("Output directory is required."); } if (!outputDirectory.exists()) { throw new IllegalStateException("Output directory does not exist."); } if (!outputDirectory.isDirectory()) { throw new IllegalStateException("Invalid output directory."); } if (name == null) { throw new IllegalStateException("Name is required."); } if (displayName == null) { throw new IllegalStateException("Display name is required."); } if (identifier == null) { throw new IllegalStateException("Identifier is required."); } if (iconFile != null) { if (!iconFile.exists()) { throw new IllegalStateException("Icon does not exist."); } if (iconFile.isDirectory()) { throw new IllegalStateException("Invalid icon."); } } if (shortVersion == null) { throw new IllegalStateException("Short version is required."); } if (signature == null) { throw new IllegalStateException("Signature is required."); } if (signature.length() != 4) { throw new IllegalStateException("Invalid signature."); } if (copyright == null) { throw new IllegalStateException("Copyright is required."); } if (mainClassName == null) { throw new IllegalStateException("Main class name is required."); } // Create the app bundle try { System.out.println("Creating app bundle: " + name); // Create directory structure File rootDirectory = new File(outputDirectory, name + ".app"); delete(rootDirectory); rootDirectory.mkdir(); File contentsDirectory = new File(rootDirectory, "Contents"); contentsDirectory.mkdir(); File macOSDirectory = new File(contentsDirectory, "MacOS"); macOSDirectory.mkdir(); File javaDirectory = new File(contentsDirectory, "Java"); javaDirectory.mkdir(); File plugInsDirectory = new File(contentsDirectory, "PlugIns"); plugInsDirectory.mkdir(); File resourcesDirectory = new File(contentsDirectory, "Resources"); resourcesDirectory.mkdir(); // // Move back to Contents/Resources/Java instead of Contents/Java [fry] // File javaDirectory = new File(resourcesDirectory, "Java"); // javaDirectory.mkdir(); // Generate Info.plist File infoPlistFile = new File(contentsDirectory, "Info.plist"); infoPlistFile.createNewFile(); writeInfoPlist(infoPlistFile); // Generate PkgInfo File pkgInfoFile = new File(contentsDirectory, "PkgInfo"); pkgInfoFile.createNewFile(); writePkgInfo(pkgInfoFile); // Copy executable to MacOS folder File executableFile = new File(macOSDirectory, executableName); copy(getClass().getResource(EXECUTABLE_NAME), executableFile); executableFile.setExecutable(true, false); // Copy localized resources to Resources folder copyResources(resourcesDirectory); // Copy runtime to PlugIns folder copyRuntime(plugInsDirectory); // Copy class path entries to Java folder copyClassPathEntries(javaDirectory); // Copy class path ref entries to Java folder copyClassPathRefEntries(javaDirectory); // Copy library path entries to MacOS folder copyLibraryPathEntries(macOSDirectory); // Copy icon to Resources folder copyIcon(resourcesDirectory); // Copy the bundle/document icons as well copyBundleIcons(resourcesDirectory); } catch (IOException exception) { throw new BuildException(exception); } } private void copyResources(File resourcesDirectory) throws IOException { // Unzip res.zip into resources directory InputStream inputStream = getClass().getResourceAsStream("res.zip"); ZipInputStream zipInputStream = new ZipInputStream(inputStream); try { ZipEntry zipEntry = zipInputStream.getNextEntry(); while (zipEntry != null) { File file = new File(resourcesDirectory, zipEntry.getName()); if (zipEntry.isDirectory()) { file.mkdir(); } else { OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(file), BUFFER_SIZE); try { int b = zipInputStream.read(); while (b != -1) { outputStream.write(b); b = zipInputStream.read(); } outputStream.flush(); } finally { outputStream.close(); } } zipEntry = zipInputStream.getNextEntry(); } } finally { zipInputStream.close(); } } private void copyRuntime(File plugInsDirectory) throws IOException { if (runtime != null) { File runtimeHomeDirectory = runtime.getDir(); File runtimeContentsDirectory = runtimeHomeDirectory.getParentFile(); File runtimeDirectory = runtimeContentsDirectory.getParentFile(); // Create root plug-in directory File pluginDirectory = new File(plugInsDirectory, runtimeDirectory.getName()); pluginDirectory.mkdir(); // Create Contents directory File pluginContentsDirectory = new File(pluginDirectory, runtimeContentsDirectory.getName()); pluginContentsDirectory.mkdir(); // Copy MacOS directory File runtimeMacOSDirectory = new File(runtimeContentsDirectory, "MacOS"); copy(runtimeMacOSDirectory, new File(pluginContentsDirectory, runtimeMacOSDirectory.getName())); // Copy Info.plist file File runtimeInfoPlistFile = new File(runtimeContentsDirectory, "Info.plist"); copy(runtimeInfoPlistFile, new File(pluginContentsDirectory, runtimeInfoPlistFile.getName())); // Copy included contents of Home directory File pluginHomeDirectory = new File(pluginContentsDirectory, runtimeHomeDirectory.getName()); DirectoryScanner directoryScanner = runtime.getDirectoryScanner(getProject()); String[] includedFiles = directoryScanner.getIncludedFiles(); for (String includedFile : includedFiles) { //for (int i = 0; i < includedFiles.length; i++) { //String includedFile = includedFiles[i]; File source = new File(runtimeHomeDirectory, includedFile); File destination = new File(pluginHomeDirectory, includedFile); copy(source, destination); } } } private void copyClassPathRefEntries(File javaDirectory) throws IOException { if (classPathRef != null) { org.apache.tools.ant.types.Path classpath = (org.apache.tools.ant.types.Path) classPathRef.getReferencedObject(getProject()); Iterator<?> iter = classpath.iterator(); while (iter.hasNext()) { FileResource resource = (FileResource) iter.next(); File source = resource.getFile(); File destination = new File(javaDirectory, source.getName()); copy(source, destination); } } } private void copyClassPathEntries(File javaDirectory) throws IOException { for (FileSet fileSet : classPath) { File classPathDirectory = fileSet.getDir(); DirectoryScanner directoryScanner = fileSet.getDirectoryScanner(getProject()); String[] includedFiles = directoryScanner.getIncludedFiles(); for (String includedFile : includedFiles) { File source = new File(classPathDirectory, includedFile); File destination = new File(javaDirectory, new File(includedFile).getName()); copy(source, destination); } } } private void copyLibraryPathEntries(File macOSDirectory) throws IOException { for (FileSet fileSet : libraryPath) { File libraryPathDirectory = fileSet.getDir(); DirectoryScanner directoryScanner = fileSet.getDirectoryScanner(getProject()); String[] includedFiles = directoryScanner.getIncludedFiles(); for (String includedFile : includedFiles) { File source = new File(libraryPathDirectory, includedFile); File destination = new File(macOSDirectory, new File(includedFile).getName()); copy(source, destination); } } } private void copyIcon(File resourcesDirectory) throws IOException { if (iconFile == null) { copy(getClass().getResource(DEFAULT_ICON_NAME), new File(resourcesDirectory, DEFAULT_ICON_NAME)); } else { copy(iconFile, new File(resourcesDirectory, iconFile.getName())); } } private void copyBundleIcons(File resourcesDirectory) throws IOException { for (BundleDocument bundleDocument : bundleDocuments) { if (bundleDocument.hasIcon()) { File iconFile = bundleDocument.getIconFile(); copy(iconFile, new File(resourcesDirectory, iconFile.getName())); } } } private void writeInfoPlist(File file) throws IOException { FileOutputStream output = new FileOutputStream(file); PropertyLister plist = new PropertyLister(output); // Get started, write all necessary header info and open plist element plist.writeStartDocument(); // Begin root dictionary plist.writeStartDictElement(); // Write bundle properties plist.writeProperty("CFBundleDevelopmentRegion", "English"); plist.writeProperty("CFBundleExecutable", executableName); plist.writeProperty("CFBundleIconFile", (iconFile == null) ? DEFAULT_ICON_NAME : iconFile.getName()); plist.writeProperty("CFBundleIdentifier", identifier); plist.writeProperty("CFBundleDisplayName", displayName); plist.writeProperty("CFBundleInfoDictionaryVersion", "6.0"); plist.writeProperty("CFBundleName", name); plist.writeProperty("CFBundlePackageType", OS_TYPE_CODE); plist.writeProperty("CFBundleShortVersionString", shortVersion); plist.writeProperty("CFBundleVersion", version); plist.writeProperty("CFBundleSignature", signature); plist.writeProperty("NSHumanReadableCopyright", copyright); if (getInfo != null) { plist.writeProperty("CFBundleGetInfoString", getInfo); } if (applicationCategory != null) { plist.writeProperty("LSApplicationCategoryType", applicationCategory); } if (minimumSystem != null) { plist.writeProperty("LSMinimumSystemVersion", minimumSystem); } if (highResolutionCapable) { plist.writeKey("NSHighResolutionCapable"); plist.writeBoolean(true); } if (runtime != null) { plist.writeProperty("JVMRuntime", runtime.getDir().getParentFile().getParentFile().getName()); } if (privileged != null) { plist.writeProperty("JVMRunPrivileged", privileged); } if (workingDirectory != null) { plist.writeProperty("WorkingDirectory", workingDirectory); } // Write main class name plist.writeProperty("JVMMainClassName", mainClassName); // Write CFBundleDocument entries plist.writeKey("CFBundleDocumentTypes"); plist.writeStartArrayElement(); for (BundleDocument bundleDocument: bundleDocuments) { plist.writeStartDictElement(); plist.writeKey("CFBundleTypeExtensions"); plist.writeStartArrayElement(); for (String extension : bundleDocument.getExtensions()) { plist.writeString(extension); } plist.writeEndElement(); if (bundleDocument.hasIcon()) { plist.writeKey("CFBundleTypeIconFile"); plist.writeString(bundleDocument.getIconName()); } plist.writeKey("CFBundleTypeName"); plist.writeString(bundleDocument.getName()); plist.writeKey("CFBundleTypeRole"); plist.writeString(bundleDocument.getRole()); plist.writeKey("LSTypeIsPackage"); plist.writeBoolean(bundleDocument.isPackage()); plist.writeEndElement(); } plist.writeEndElement(); // Write architectures plist.writeKey("LSArchitecturePriority"); plist.writeStartArrayElement(); for (String architecture : architectures) { plist.writeString(architecture); } plist.writeEndElement(); // Write Environment plist.writeKey("LSEnvironment"); plist.writeStartDictElement(); plist.writeKey("LC_CTYPE"); plist.writeString("UTF-8"); plist.writeEndElement(); // Write options plist.writeKey("JVMOptions"); plist.writeStartArrayElement(); for (String option : options) { plist.writeString(option); } plist.writeEndElement(); // Write arguments plist.writeKey("JVMArguments"); plist.writeStartArrayElement(); for (String argument : arguments) { plist.writeString(argument); } plist.writeEndElement(); // End root dictionary plist.writeEndElement(); // Close out the plist plist.writeEndDocument(); } private void writePkgInfo(File file) throws IOException { Writer out = new BufferedWriter(new FileWriter(file)); try { out.write(OS_TYPE_CODE + signature); out.flush(); } finally { out.close(); } } private static void delete(File file) throws IOException { Path filePath = file.toPath(); if (Files.exists(filePath, LinkOption.NOFOLLOW_LINKS)) { if (Files.isDirectory(filePath, LinkOption.NOFOLLOW_LINKS)) { File[] files = file.listFiles(); for (int i = 0; i < files.length; i++) { delete(files[i]); } } Files.delete(filePath); } } private static void copy(URL location, File file) throws IOException { try (InputStream in = location.openStream()) { Files.copy(in, file.toPath(), StandardCopyOption.REPLACE_EXISTING); } } private static void copy(File source, File destination) throws IOException { Path sourcePath = source.toPath(); Path destinationPath = destination.toPath(); destination.getParentFile().mkdirs(); Files.copy(sourcePath, destinationPath, StandardCopyOption.REPLACE_EXISTING, LinkOption.NOFOLLOW_LINKS); if (Files.isDirectory(sourcePath, LinkOption.NOFOLLOW_LINKS)) { String[] files = source.list(); for (int i = 0; i < files.length; i++) { String file = files[i]; copy(new File(source, file), new File(destination, file)); } } } }